Release 1.1.0 - #237
Merged
Merged
Conversation
CssMediaQueryList.ComputeMatched returned a constant false, so window.matchMedia(...).IsMatched answered false for every query, including "all" and the empty query, which always match. It now validates the media list against the render device from the browsing context, reusing the very same evaluation that @media rules already go through for the cascade, and falls back to DefaultRenderDevice when no device is registered - the same fallback GetComputedStyle uses. Reported in AngleSharp/AngleSharp#1307. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
CssColorValue always serializes through rgba(), so an opaque color comes out as rgba(r, g, b, 1) where the CSSOM serialization rules ask for rgb(r, g, b). Changing that by default would be breaking, so this adds UseSpecSerialization next to UseHex: off by default, and when switched on an opaque color serializes as rgb(r, g, b) while anything with an alpha below 1 keeps rgba(r, g, b, a). UseHex still wins when both are active. Reducing a color that was written as a named color back to its name is a separate step and is not part of this change. Closes #227. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
Evaluate the media query list in matchMedia against the render device
Add an opt-in switch for CSSOM-compliant color serialization
Adds IRenderDevicePreferences, a small interface with a single IReadOnlyDictionary<String, String> Preferences member that DefaultRenderDevice implements, so a host can say which user preferences its device carries without every existing IRenderDevice implementation having to change. A generic PreferenceFeatureValidator is registered for prefers-color-scheme, prefers-reduced-motion, prefers-reduced-transparency, prefers-contrast, prefers-reduced-data, forced-colors and display-mode, and hover/any-hover and pointer/any-pointer now read the dictionary when it carries them, while keeping their previous answer when it does not. A key that is not set leaves its feature unknown, i.e., the query does not match. Fixes #234 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
Review feedback on #235: the convenience layer over IRenderDevice is useful to hosts that configure a DefaultRenderDevice, so the class is public and documented. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NqCcJrL3MJecCPRBMQsZyC
Answer the user-preference media features from the render device
The calc add/sub/mul/div expressions create the resulting metric value via Activator.CreateInstance(x.GetType(), result). The trimmer cannot see that call target, so the single-Double constructors of the built-in metric values were removed, making every calc() computation throw MissingMethodException in NativeAOT (and trimmed) applications. The reflection call now lives in a single helper that roots the public constructors of all built-in metric values via DynamicDependency, so the behavior matches the JIT one. ICssMetricValue is public, hence external implementations stay supported - they just have to preserve their own constructor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a2612f6f-7d43-40de-a31f-96a2600e6f81
Fix calc() computation in trimmed / NativeAOT applications
CssTokenizer.ContentFrom re-scans the raw source to recover a declaration
value or an at-rule prelude, and breaks at the first ';', '{' or '}'. It
special-cased quoted strings but knew nothing about url tokens, where all
three characters are legal content.
As a result "url(data:image/svg+xml;base64,...)" was cut at the first
semicolon and became url("data:image/svg+xml"); the remainder failed to
re-tokenize as a declaration and was dropped, so a round-trip through
CssText silently destroyed the asset. Unquoted data URIs are emitted by
every major bundler, so this affected many real stylesheets. Via the
GetArgument path the same flaw discarded whole rules: a @supports
condition containing url(a;b) lost its entire rule.
Teach ContentFrom about url tokens: on an ident "url" immediately followed
by '(', consume through the matching unescaped ')' before the break check
resumes. Behaviour was validated against Chrome's CSSOM over ~40 inputs,
which pinned down three subtleties:
- The ident must be immediately followed by '(' - "myurl(", "-url(" and
"url\t(" are ordinary function tokens where ';' does terminate the
declaration, so a plain substring match would introduce a new bug.
- url( followed by a quote is a function token, not a url token: the
string wins and a ')' inside it does not close the url. The scan skips
whitespace after '(' and defers to the existing string handling.
- Bad-url cases such as url(a b) still consume through the matching ')',
so a single "consume to unescaped ')'" rule extracts the correct span
in every case.
Escapes inside the url are honoured, so url(a\;b) also parses correctly
where it previously produced url("a\\").
The remaining divergences from Chrome (url(a b), url(a(b), url()) live in
UrlUQ/UrlBad and are unchanged by this commit.
The @supports condition parser compared the "and" / "or" keywords with ordinal equality, so an uppercase or mixed-case keyword silently discarded the whole conditional group and every rule inside it. The keyframe selector parser did the same for "from" / "to", leaving the rule in the CSSOM with a null key. Switched both to the case-insensitive Isi helper, including the chain continuation in Scan, which compared each subsequent keyword against the raw text of the first one.
The calc() operand parser recursed into itself for the right operand of
every operator, which made the resulting expression tree right
associative. Chains of two or more identical operators were therefore
evaluated in the wrong order: calc(10px - 2px - 3px) built
Sub(10px, Sub(2px, 3px)) and computed to 11px instead of 5px, and
calc(100px / 2 / 5) built 100 / (2 / 5) instead of (100 / 2) / 5.
Mixed precedence expressions happened to come out right, so only chains
of same precedence operators were affected. No parse error was raised;
the wrong number was simply handed to the computed style.
Replace the four right recursive levels with two iterative loops that
fold the operands to the left, matching the grammar
expression := term (('+' | '-') term)*
term := factor (('*' | '/') factor)*
This also collapses the artificial split between the Add/Sub and the
Mul/Div levels, which is what introduced the asymmetry. Serialization is
unaffected, as CssText concatenates the operands in order without
adding parentheses.
Expected values are taken from Chrome via getComputedStyle.
Two independent defects made a division in calc() report a value with the wrong unit. Dividing two values that share a unit cancels the unit out and yields a plain number, but CssCalcDivExpression kept the unit of the left operand. calc(10px / 20px) computed to 0.5px instead of 0.5, so declarations such as opacity, flex-grow, z-index or line-height ended up with a length where a number was expected. CssMetricValueExtensions.WithValue creates the result through Activator.CreateInstance(type, value), and the single argument constructor of CssLengthValue defaults to pixels. Any unitless length was therefore turned into a length in pixels: calc(1 / 4) computed to 0.25px rather than 0.25. Preserve the unit of the template instead; this covers multiplication too, where calc(2 * 3) computed to 6px. Expected values are taken from Chrome via getComputedStyle, which reports 0.5 for opacity: calc(10px / 20px), 2 for flex-grow: calc(100px / 50px) and 150px for width: calc(100px / 2px * 3px).
My previous commit fixed where a url token *ends*. This fixes what happens
when one is invalid, which was a separate defect with the same symptom
class: values that browsers reject were being accepted as garbage.
Three divergences from Chrome, all rooted in the fact that a bad url had
no way to be reported as a failure:
CssUriParser.Bad() returned a CssUrlValue built from whatever characters
it had scanned past, so an invalid url produced a plausible-looking but
wrong value instead of failing. url(a b) became url("ab") and
url(a(b) became url("a(b)"); browsers drop the declaration in both cases.
Bad() now returns null and ParseUri rewinds the source, so the url() is
seen as unparsed rather than as absent - rewinding matters, because merely
consuming the bad url let "background: url(a b) red" silently re-parse as
"background: red" instead of being dropped.
ParseUri did not consume the ')' of an empty url(), leaving the source
mid-value so the declaration was rejected. url() is valid and means the
empty URL, so it now parses as url("").
CssTokenizer.NewUrl accepted a "bad" parameter and ignored it, so no
bad-url token could ever exist at the sheet level and UrlBad's scanned-over
characters became the url's content. "@import url(a b)" imported the
garbage href "a b)". A BadUrl token type now carries the distinction, and
UrlBad discards the remnants it consumes. Per the spec, EOF ends a url
token rather than invalidating it, so the two EOF paths that flagged bad
no longer do - "@import url(abc" still imports "abc".
Consequences at the rule level, matching Chrome:
- @import with a bad url is dropped instead of importing a garbage href.
- @namespace with a bad url is dropped. Fixing this forced a decision on
the string form, since one condition governs both: @namespace accepted
only a url token, so "@namespace x "http://foo"" silently produced an
empty namespace URI. It now accepts a string as the spec requires.
Behaviour was verified against Chrome's CSSOM over the full 30-case matrix
(both fixes together); no divergence remains.
ParseInlineStyleWithToleratedInvalidValueShouldReturnThatValue asserted the
old lenient recovery for url(javascript:alert(1)) - an unquoted url with a
'(' in it, i.e. exactly the url(a(b) case. Chrome drops that declaration,
so the test now documents that, and a companion test covers the quoted form
which is valid and still round-trips. The tolerance it relied on came from
Bad(), not from IsIncludingUnknownDeclarations, which only governs unknown
property names.
Resolve per-element custom-property dependency components iteratively before inheritance, including unused fallback edges. Preserve computed token values, substitute complete consumer and shorthand values, and apply computed-value defaults without recursive variable evaluation. Add regression coverage for cycles, inheritance, shared rules, CSSOM mutation, token boundaries, long chains, nested fallbacks, and bounded expansion. Document the substitution limit. Fixes #241 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…sociativity-96200d Fix operator associativity and result units in calc()
…tivity-c30828 Fix case-sensitive matching of and/or and from/to keywords in CSS parsers
…-68b99b Fix unquoted and invalid url() handling
Keep variable resolution inside computed-style preparation; leave raw cascades and render-tree specified styles unchanged. Restore nested CssVarValue fallback trees and direct References-based computation, retaining iterative parsing, serialization, and fallback traversal. Honor caller-modified references in custom-property and shorthand resolution. Add compatibility regressions for raw declarations, public parser cursor and value-tree behavior, mutable references, and deep public fallbacks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fix circular custom properties during computed style resolution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Types of Changes
Prerequisites
Please make sure you can check the following two boxes:
Contribution Type
What types of changes does your code introduce? Put an
xin all the boxes that apply:Description
Thanks to all the contributors in this one we have fixed a set of bugs and added some great enhancements. Most importantly, we improved media queries and fixed some CSS computation bugs w.r.t. media features / types, as well as in the CSS variables (
calc) space.Through the new
IRenderDevicePreferencesinterface we are capable of dynamically adding platform descriptors, which are then used throughout the CSS evaluation (right now exclusive to media computations).